﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

using BizElements.Core;
using BizElements.BusinessLayer;
using BizElements.Web;

namespace BizElements.Web.UI
{
    /// <summary>Component that edits and displays a collection of business objects. Uses Repeater control and custom user controls that implement IDataObjectUI interface.</summary>
    public partial class CollectionRepeater : UserControl, ICollectionEditor
    {
        // ReadOnly value in previous PostBack.
        bool wasReadOnly;
        // Bottons buttons visibility is automatically applied in PreRender unless this flag is set to true which means that another method has changed visibility according to its own rules.
        bool suppressApplyBottomButtonsVisibility;

        #region Init, ControlState.

        /// <summary>Register the controls as one whose state must be persisted.</summary>
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            this.Page.RegisterRequiresControlState(this);
        }

        /// <summary>Saves ReadOnly flag to control state.</summary>
        protected override object SaveControlState()
        {
            object baseClassContribution = base.SaveControlState();
            CollectionRepeaterState state = new CollectionRepeaterState() { Collection = this.Collection, ReadOnly = this.ReadOnly };
            Pair baseStateAndThisState = new Pair(baseClassContribution, state);
            return baseStateAndThisState;
        }

        /// <summary>Loads previous value for ReadOnly flag.</summary>
        protected override void LoadControlState(object savedState)
        {
            Pair baseStateAndThisState = savedState as Pair;
            if (baseStateAndThisState != null)
            {
                base.LoadControlState(baseStateAndThisState.First);
                CollectionRepeaterState deserializedState = (CollectionRepeaterState)baseStateAndThisState.Second;
                this.Collection = deserializedState.Collection;
                this.wasReadOnly = deserializedState.ReadOnly;
            }
        }

        /// <summary>Contains data that is serialized to view-state: Collection and ReadOnly flag.</summary>
        [Serializable]
        class CollectionRepeaterState
        {
            public IList Collection { get; set; }
            public bool ReadOnly { get; set; }
        }

        #endregion

        #region PreRender, show/hide buttons.

        /// <summary>Hides all buttons if in read-only mode.</summary>
        protected override void OnPreRender(EventArgs e)
        {
            if (this.ReadOnly)
            {
                HideAllButtons();
            }
            else
            {
                if (!this.suppressApplyBottomButtonsVisibility)
                    ApplyBottomButtonsVisibility();

                // CollectionRepeater was read-only but now it is editable --> show all item buttons.
                if (this.wasReadOnly)
                    ShowAllItemButtonsRows();
            }
        }

        #endregion

        #region Events.

        /// <summary>CommandName passed when an event is fired.</summary>
        public string CommandName { get; set; }

        /// <summary>CommandArgument passed when an event is fired.</summary>
        public string CommandArgument { get; set; }

        /// <summary>Occurs when a new item sould be added via AddItems method.</summary>
        public event CommandEventHandler NewItem;

        /// <summary>Occurs when user request collection to be persisted.</summary>
        public event CommandEventHandler SaveCollection;

        /// <summary>Occurs when user requests that the collection is refetched, ie. when any unsaved changes should be discarded.</summary>
        public event CommandEventHandler RefetchCollection;

        /// <summary>Occurs when parent component must ensure that all collection items have parent entities which are displayed in UI and that all items implement ISelfValidatingObject interface.</summary>
        public event CommandEventHandler ConvertCollectionItemsToBusinessObject;

        /// <summary>Occurs for each created entity user control. Allows you to customize it after it's been bound to entity and added to repeater.</summary>
        public event EventHandler<EventArgs<IDataObjectUI>> EntityUserControlCreated;

        #endregion

        #region ICollectionEditor properties.

        /// <summary>Not supported - returns <b>false</b>. This controls can only edit one entity at a time.</summary>
        public bool AlwaysEditBoundFields
        {
            get { return false; }
            set { }
        }

        /// <summary>Control title.</summary>
        public string Caption { get; set; }

        /// <summary>Collection of bound business objects. Objects must either implement <see cref="BizElements.BusinessLayer.ISelfValidatingObject"/> interface or may be converted to objects that implement it when the <see cref="ConvertCollectionItemsToBusinessObject"/> event is fired.</summary>
        public IList Collection { get; set; }

        /// <summary>Indicates whether the component is in read-only mode, ie. whether the collection is editable. The value may be changed
        /// in design time (ASPC/ASCX file) or in Init phase. No affect after Init phase because that's when the fields are bound to the underlying grid.</summary>
        public bool ReadOnly { get; set; }

        /// <summary>Returns empty array. CollectionRepeater does not support "selected item" concept.</summary>
        public string[] SelectedValues
        {
            get { return new string[0]; }
        }

        #endregion

        #region Properties - child controls.

        /// <summary>Gets or sets relative path of the entity user control. The control must implement <b>IDataObjectUI</b> interface.</summary>
        public string EntityUserControlPath { get; set; }

        /// <summary>Repeater to which collection is bound.</summary>
        public Repeater Grid
        {
            get { return this.uiRepeater; }
        }

        bool newItemVisible = true;

        /// <summary>Indicates whether "New item" button is visible. If set to true then an event handler for NewItem event must be wired.</summary>
        public bool NewItemVisible
        {
            get { return this.newItemVisible; }
            set { this.newItemVisible = value; }
        }

        bool editItemVisible = true;

        /// <summary>Indicates whether "Edit selected item(s)" button is visible.</summary>
        public bool EditItemVisible
        {
            get { return this.editItemVisible; }
            set { this.editItemVisible = value; }
        }

        bool removeItemVisible = true;

        /// <summary>Indicates whether "Remove selected item(s)" button is visible.</summary>
        public bool RemoveItemVisible
        {
            get { return this.removeItemVisible; }
            set { this.removeItemVisible = value; }
        }

        /// <summary>Indicates whether "Save collection" button is visible. If set to true then an event handler for SaveCollection event must be wired.</summary>
        public bool SaveCollectionVisible { get; set; }

        /// <summary>Indicates whether "Refetch collection" button is visible. If set to true then an event handler for RefetchCollection event must be wired.</summary>
        public bool RefetchCollectionVisible { get; set; }

        #endregion

        #region ICollectionEditor methods.

        /// <summary>Adds new entity to collection and edits it. Only the new entity will be in edit mode. Not allowed if existing entities are invalid.</summary>
        /// <param name="newItems">New object. Ensure that all required but non-editable fields have been set to valid values before calling this method.</param>
        public void AddItems(params ISelfValidatingObject[] newItems)
        {
            // Cannot add new items before existing errors have been corrected.
            if (HasErrors())
            {
                JScript.Alert(UiMessages.CorrectDisplayedErrors);
                return;
            }

            if (ArrayUtil.IsNullOrEmpty(newItems))
                return;

            if (newItems.Length > 1)
                throw new ArgumentException("New items array must contain exactly one item.", "newItems");

            foreach (ISelfValidatingObject oldEntity in this.Collection)
                oldEntity.IsReadOnly = true;

            this.Collection.Add(newItems[0]);
            Refresh();
            var lastRepeaterItem = ArrayUtil.GetLastObject<RepeaterItem>(this.uiRepeater.Items);
            EditItem(lastRepeaterItem);
        }

        /// <summary>Checks if any of the bound entities is invalid.</summary>
        /// <value><b>true</b> if errors are found; otherwise <b>false</b></value>
        public bool HasErrors()
        {
            foreach (ISelfValidatingObject entity in this.Collection)
            {
                var errors = entity.Validate(ValidationLevel.RulesThatDontRequireDatabaseAccess);
                if (errors.HasBrokenRules)
                    return true;
            }

            return false;
        }

        /// <summary>Checks if there are any new of edited items that haven't submitted and accepted.</summary>
        /// <value><b>true</b> if an entity whose <b>IsReadOnly</b> property is set to <b>false</b> is found.</value>
        public bool HasUnsubmittedChanges()
        {
            foreach (ISelfValidatingObject entity in this.Collection)
            {
                if (!entity.IsReadOnly)
                    return true;
            }

            return false;
        }

        /// <summary>Hides all butons, ie. sets their Visible property to <b>false</b>.</summary>
        public void HideAllButtons()
        {
            this.uiNewItem.Visible = false;
            this.uiSaveCollection.Visible = false;
            this.uiRefetchCollection.Visible = false;
            foreach (Control buttonsRow in GetItemButtonsRowList())
                buttonsRow.Visible = false;
        }

        /// <summary>Refreshes the control, ie. rebinds the collection.</summary>
        public void Refresh()
        {
            BindCollection();
        }

        /// <summary>Binds collection and refreshes all created entity user controls.</summary>
        private void BindCollection()
        {
            // Allow developers to optionally finish object initalization before rebinding. 
            // Eg. fetch parents, auto-compute properties...
            if (this.ConvertCollectionItemsToBusinessObject != null)
                ConvertCollectionItemsToBusinessObject(this, new CommandEventArgs(this.CommandName, this.CommandArgument));

            this.uiRepeater.DataSource = this.Collection;
            this.uiRepeater.DataBind();
            foreach (IDataObjectUI entityControl in GetEntityControlList())
                entityControl.Refresh();
        }

        /// <summary>Rejects all new and currently edited items that haven't been accepted yet. May not be called before the Load phase is completed.</summary>
        public void RejectUnsubmittedChanges()
        {
            // First we create a list of indexes to rejects.
            // Rejecting must be executed in reverse order because rejection of new entity means that it may be removed from the (end of) list.
            List<int> indexesOfEntitiesToReject = new List<int>();
            for (int i = 0; i < this.Collection.Count; i++)
            {
                ISelfValidatingObject currEntity = (ISelfValidatingObject)this.Collection[i];
                if (!currEntity.IsReadOnly)
                    indexesOfEntitiesToReject.Add(i);
            }

            for (int i = indexesOfEntitiesToReject.Count - 1; i >= 0; i--)
            {
                int indexOfItemToReject = indexesOfEntitiesToReject[i];
                RejectChanges(this.uiRepeater.Items[indexOfItemToReject]);
            }
        }

        #endregion

        #region SaveCollection, RefetchCollection.

        /// <summary>Persists collection on user request.</summary>
        protected void uiSaveCollection_Click(object sender, EventArgs e)
        {
            if (this.SaveCollection != null)
                SaveCollection(this, new CommandEventArgs(this.CommandName, this.CommandArgument));
        }

        /// <summary>Refetches collection on user request discarding any unsaved data.</summary>
        protected void uiRefetchCollection_Click(object sender, EventArgs e)
        {
            if (this.RefetchCollection != null)
                RefetchCollection(this, new CommandEventArgs(this.CommandName, this.CommandArgument));
        }

        #endregion

        #region Create item controls.

        /// <summary>Creates entity user controls and binds to entities. Called for each item when data is bound/refreshed and automatically after postback when collection is deserialized from viewstate.</summary>
        protected void uiRepeater_ItemCreated(object sender, RepeaterItemEventArgs e)
        {
            if (string.IsNullOrEmpty(this.EntityUserControlPath))
                throw new InvalidOperationException("EntityUserControlPath property must be set before items can be created.");

            var itemType = e.Item.ItemType;
            if (itemType == ListItemType.Item || itemType == ListItemType.AlternatingItem || itemType == ListItemType.EditItem || itemType == ListItemType.SelectedItem)
            {
                Control btnRemoveItem = e.Item.FindControl("uiRemoveItem");
                btnRemoveItem.Visible = this.RemoveItemVisible;

                IDataObjectUI entityControl = CreateEntityUserControl(e.Item.ItemIndex);
                PlaceHolder entityUiContainer = (PlaceHolder)e.Item.FindControl("uiItemPlaceHolder");
                entityUiContainer.Controls.Add((Control)entityControl);
                if (this.EntityUserControlCreated != null)
                    EntityUserControlCreated(this, new EventArgs<IDataObjectUI>(entityControl));
            }
        }

        /// <summary>Creates entity user controls and binds to entity.</summary>
        /// <param name="itemIndex">Index of entity in the collection.</param>
        private IDataObjectUI CreateEntityUserControl(int itemIndex)
        {
            IDataObjectUI entityControl = (IDataObjectUI)LoadControl(this.EntityUserControlPath);
            entityControl.HeaderAndFooterHidden = true;

            IDataObject bizObject = (IDataObject)this.Collection[itemIndex];
            entityControl.BizObject = bizObject;
            entityControl.ReadOnly = bizObject.IsReadOnly;
            return entityControl;
        }

        #endregion

        #region NewItem, EditItem, RemoveItem, AcceptChanges, RejectChanges.

        /// <summary>Add new item on user request. Parent component initializes a new entity and must add it via AddItems() method.</summary>
        protected void uiNewItem_Click(object sender, EventArgs e)
        {
            if (this.NewItem != null)
                NewItem(this, new CommandEventArgs(this.CommandName, this.CommandArgument));
        }

        /// <summary>Processes CollectionRepeater item command. Possible commands: EditItem, RemoveItem, AcceptChanges, RejectChanges.</summary>
        protected void uiRepeater_ItemCommand(object source, RepeaterCommandEventArgs e)
        {
            switch (e.CommandName)
            {
                case "EditItem":
                    EditItem(e.Item);
                    break;

                case "RemoveItem":
                    RemoveItem(e.Item);
                    break;

                case "AcceptChanges":
                    AcceptChanges(e.Item);
                    break;

                case "RejectChanges":
                    RejectChanges(e.Item);
                    break;
            }
        }

        /// <summary>Edits entity bound to provided item. Rejects to all other items because only one item can be edited at a time.</summary>
        /// <param name="repeaterItem">Repeater item.</param>
        private void EditItem(RepeaterItem repeaterItem)
        {
            // Only one item can be edited. Reject changes to other items.
            RejectUnsubmittedChanges();

            // Due to ViewState, in PostBacks the entities bound to UCs are not the same as instances in Collection property.
            // Collection property is first deserialized in LoadControlState but containing forms/UCs may change it after Init phase.
            // Edit event occurs after Load phase and now we have to ensure that entity UC is working on entity that belong to current collection.
            var entityControl = GetEntityControlAt(repeaterItem.ItemIndex);
            entityControl.BizObject = (IDataObject)this.Collection[repeaterItem.ItemIndex];
            entityControl.ForceAccessMode(FieldAccessMode.Edit, /*refresh*/ true);
            SwitchItemButtons(repeaterItem, /*enter edit mode*/ true);

            // Hide buttons for all other items.
            var buttonRows = GetItemButtonsRowList();
            for (int i = 0; i < buttonRows.Count; i++)
            {
                bool isOtherItem = (i != repeaterItem.ItemIndex);
                if (isOtherItem)
                    buttonRows[i].Visible = false;
            }
        }

        /// <summary>Parses and validates business object bound to provided item.</summary>
        /// <param name="repeaterItem">Repeater item.</param>
        private void AcceptChanges(RepeaterItem repeaterItem)
        {
            var entityControl = GetEntityControlAt(repeaterItem.ItemIndex);
            bool noErrors = entityControl.PopulateAndValidateBusinessObject();
            if (noErrors)
            {
                // This won't work if PK is not autogenerated (auto-identity, sequence, guid).
                // Because biz objects user PKs to determine what to do during Save().
                this.Collection[repeaterItem.ItemIndex] = entityControl.BizObject;

                // Allow developers to optionally finish object initalization before rebinding. 
                // Eg. fetch parents, auto-compute properties...
                if (this.ConvertCollectionItemsToBusinessObject != null)
                    ConvertCollectionItemsToBusinessObject(this, new CommandEventArgs(this.CommandName, this.CommandArgument));

                entityControl.ForceAccessMode(FieldAccessMode.ReadOnly, /*refresh*/ true);
                SwitchItemButtons(repeaterItem, /*exit edit mode*/ false);
                ShowAllItemButtonsRows();
            }
            else
            {
                // Remain in edit mode.
                entityControl.ForceAccessMode(FieldAccessMode.Edit, /*refresh*/ true);
                SwitchItemButtons(repeaterItem, /*enter edit mode*/ true);
            }
        }

        /// <summary>Rejects new/changed data.</summary>
        /// <param name="repeaterItem">Repeater item.</param>
        private void RejectChanges(RepeaterItem repeaterItem)
        {
            // Due to ViewState, in PostBacks the entities bound to UCs are not the same as instances in Collection property.
            // Collection property is first deserialized in LoadControlState but containing forms/UCs may change it after Init phase.
            var entityControl = GetEntityControlAt(repeaterItem.ItemIndex);
            entityControl.BizObject = (IDataObject)this.Collection[repeaterItem.ItemIndex];

            // Rollback to previous version. If previous version is invalid then remain in edit mode.
            bool isOldVersionInvalid = entityControl.BizObject.Validate(ValidationLevel.RulesThatDontRequireDatabaseAccess).HasBrokenRules;
            if (isOldVersionInvalid)
            {
                // Remain in edit mode.
                entityControl.ForceAccessMode(FieldAccessMode.Edit, /*refresh*/ true);
                SwitchItemButtons(repeaterItem, /*enter edit mode*/ true);
            }
            else
            {
                entityControl.ForceAccessMode(FieldAccessMode.ReadOnly, /*refresh*/ true);
                SwitchItemButtons(repeaterItem, /*exit edit mode*/ false);
                ShowAllItemButtonsRows();
            }
        }

        /// <summary>Removes an item and rebinds collection.</summary>
        /// <param name="repeaterItem">Repeater item.</param>
        private void RemoveItem(RepeaterItem repeaterItem)
        {
            this.Collection.RemoveAt(repeaterItem.ItemIndex);
            Refresh();
            ApplyBottomButtonsVisibility();
        }

        /// <summary>Switches buttons visibility depending on whether an item is entering edit mode.</summary>
        /// <param name="repeaterItem">Repeater item.</param>
        /// <param name="isEditing">Indicates if the item is in edit mode.</param>
        private void SwitchItemButtons(RepeaterItem item, bool isEditing)
        {
            Control btnEditItem = item.FindControl("uiEditItem");
            Control btnAcceptChanges = item.FindControl("uiAcceptChanges");
            Control btnRejectChanges = item.FindControl("uiRejectChanges");
            Control btnRemoveItem = item.FindControl("uiRemoveItem");

            if (isEditing)
            {
                btnAcceptChanges.Visible = btnRejectChanges.Visible = true;
                btnEditItem.Visible = false;

                // Always show remove item for new entities because this is the only way to "rollback", i.e. remove new entity.
                IDataObject entity = this.Collection[item.ItemIndex] as IDataObject;
                if (entity != null && entity.IsNew)
                    btnRemoveItem.Visible = true;
                else
                    btnRemoveItem.Visible = this.RemoveItemVisible;

                HideBottomButtons();
                this.suppressApplyBottomButtonsVisibility = true;
            }
            else
            {
                btnAcceptChanges.Visible = btnRejectChanges.Visible = false;
                btnEditItem.Visible = this.EditItemVisible;
                btnRemoveItem.Visible = this.RemoveItemVisible;
                ApplyBottomButtonsVisibility();
            }
        }

        #endregion

        #region Utility: control management.

        /// <summary>Gets entity user control at the specified index.</summary>
        /// <param name="index">Zero base index.</param>
        /// <returns>User control bound to item at the specified index.</returns>
        private IDataObjectUI GetEntityControlAt(int index)
        {
            return GetEntityControlList()[index];
        }

        /// <summary>Gets a list of all created entity user controls in the same order as collection items.</summary>
        /// <returns>A list of user control that implement <b>IDataObjectUI</b> interface.</returns>
        private List<IDataObjectUI> GetEntityControlList()
        {
            var allEntityControls = new List<IDataObjectUI>();
            foreach (RepeaterItem itemControls in this.uiRepeater.Items)
            {
                PlaceHolder entityUiContainer = (PlaceHolder)itemControls.FindControl("uiItemPlaceHolder");
                IDataObjectUI entityControl = (IDataObjectUI)entityUiContainer.Controls[0];
                allEntityControls.Add(entityControl);
            }

            return allEntityControls;
        }

        /// <summary>Gets a list of row (container controls) that contain item buttons (edit, remove, accept changes, reject changes).</summary>
        /// <returns>A list of controls.</returns>
        private List<Control> GetItemButtonsRowList()
        {
            var allButtonRows = new List<Control>();
            foreach (RepeaterItem itemControls in this.uiRepeater.Items)
            {
                Control buttonsRow = itemControls.FindControl("uiItemButtonsRow");
                allButtonRows.Add(buttonsRow);
            }

            return allButtonRows;
        }

        /// <summary>Show all item button rows.</summary>
        private void ShowAllItemButtonsRows()
        {
            foreach (Control buttonRow in GetItemButtonsRowList())
                buttonRow.Visible = true;
        }

        /// <summary>Apply visibility setting to bottom row buttons: "New item", "Save collection" and "Refetch" collection buttons.</summary>
        private void ApplyBottomButtonsVisibility()
        {
            this.uiNewItem.Visible = this.NewItemVisible;
            this.uiSaveCollection.Visible = this.SaveCollectionVisible;
            this.uiRefetchCollection.Visible = this.RefetchCollectionVisible;
        }

        /// <summary>Hides bottom row buttons: "New item", "Save collection" and "Refetch" collection buttons.</summary>
        private void HideBottomButtons()
        {
            this.uiNewItem.Visible = this.uiSaveCollection.Visible = this.uiRefetchCollection.Visible = false;
        }

        #endregion
    }
}